using System;
using System.Collections;
using System.Collections.Generic;
using System.Text;
using System.Xml;

using SlimDX;
using SlimDX.Direct3D9;

using miciv;
using micivCore.Data;
using miciv.Rendering;

namespace micivCore.Components
{
    public interface IMapCommunicator
    {
        Vector2 ComputeMapCoordToWorld(MapCoords _mapCoords);
        void ComputeWorldToMapCoord(float _fX, float _fY, out MapCoords _mapCoords);
        void SetVisibleMapTile(MapCoords _mapCoords, string _strRenderable, bool _bState);
        void AttachUnitToMapTile(MapCoords _mapCoords, IUnit _unit);
        void DetachUnitToMapTile(MapCoords _mapCoords, IUnit _unit);
        MapTile GetMapTile(MapCoords _mapCoords);
        void GetNeighbors(MapCoords _mapCoords, ref List<MapTile> _lstNeighbors);
    }

    public class CoMapProto : ComponentPrototypeBase<CoMapManager>
    {
        #region Properties

        protected uint m_mapWidth;
        public uint MapWidth
        {
            get
            {
                return this.m_mapWidth;
            }
        }

        protected uint m_mapHeight;
        public uint MapHeight
        {
            get
            {
                return this.m_mapHeight;
            }
        }

        protected Hashtable m_htblTiles;
        public Hashtable Tiles
        {
            get
            {
                return this.m_htblTiles;
            }
        }

        protected string m_strTileMask;
        public string TileMask
        {
            get
            {
                return this.m_strTileMask;
            }
        }

        protected TextureManager.ManagedTexture m_maskTexture;
        public TextureManager.ManagedTexture MaskTexture
        {
            get
            {
                return this.m_maskTexture;
            }
        }

        protected string m_strDefaultTileID;
        public string DefaultTileID
        {
            get
            {
                return this.m_strDefaultTileID;
            }
        }

        protected Hashtable m_htblLayout;
        public Hashtable Layout
        {
            get
            {
                return this.m_htblLayout;
            }
        }

        protected float m_fMaskRadius;
        public float MaskRadius
        {
            get
            {
                return this.m_fMaskRadius;
            }
        }

        protected Vector2 m_f2MaskOffset;
        public Vector2 MaskOffset
        {
            get
            {
                return this.m_f2MaskOffset;
            }
        }

        #endregion

        protected Vector2 m_f2CenterOffset;

        public CoMapProto()
        {
            this.m_htblTiles = new Hashtable();
            this.m_htblLayout = new Hashtable();
            this.m_f2MaskOffset = new Vector2(0.0f, 0.0f);
            this.m_f2CenterOffset = Vector2.Zero;
        }

        public MapTileDefinition GetTile(string _strID)
        {
            if (false != this.m_htblTiles.ContainsKey(_strID))
            {
                return this.m_htblTiles[_strID] as MapTileDefinition;
            }
            return null;
        }

        public MapTileDefinition GetDefaultTile()
        {
            if (false != this.m_htblTiles.ContainsKey(this.m_strDefaultTileID))
            {
                return this.m_htblTiles[this.m_strDefaultTileID] as MapTileDefinition;
            }
            return null;
        }

        override public bool Initialize(EntityPrototype _enProto, XmlElement _xmlConfig, IComponentManager _coManager)
        {
            if (false != base.Initialize(_enProto, _xmlConfig, _coManager))
            {
                // map size
                XmlElement xmlSize = _xmlConfig.SelectSingleNode("size") as XmlElement;
                this.m_mapWidth = uint.Parse(xmlSize.Attributes["x"].Value);
                this.m_mapHeight = uint.Parse(xmlSize.Attributes["y"].Value);
                // tiles library
                InitializeTiles(_xmlConfig.SelectSingleNode("tiles") as XmlElement);
                // layout
                InitializeLayout(_xmlConfig.SelectSingleNode("layout") as XmlElement);
                return true;
            }
            return false;
        }

        public Vector2 ComputePositionToWorld(MapCoords _mapCoords)
        {
            return this.ComputePositionToWorld(_mapCoords, true);
        }

        protected Vector2 ComputePositionToWorld(MapCoords _mapCoords, bool _bAddOffset)
        {
            float x = (4 * _mapCoords.X + 2 * (_mapCoords.Y % 2)) * this.MaskOffset.X;
            float y = (2 * _mapCoords.Y) * this.MaskOffset.Y;
            Vector2 f2Result = new Vector2(x, y);
            if (false != _bAddOffset)
            {
                f2Result += this.m_f2CenterOffset;
            }
            return f2Result;
        }

        public void ComputeWorldToPosition(float _fX, float _fY, out MapCoords _mapCoords)
        {
            // remove offset to get actual position on map
            _fX -= this.m_f2CenterOffset.X;
            _fY -= this.m_f2CenterOffset.Y;
            this.CoreContext.GetData<IUIManager>("UIManager").DebugText(
                new Vector2(0.0f, 0.0f),
                new Color4(1.0f, 1.0f, 1.0f, 1.0f),
                string.Format("world : {0:F2} {1:F2}", _fX, _fY));
            // compute very raw position coordinates to tile coordinates
            uint _j = (uint)((_fY / this.MaskOffset.Y) / 2.0f);
            uint _i = (uint)((_fX / this.MaskOffset.X) / 4.0f);
            this.CoreContext.GetData<IUIManager>("UIManager").DebugText(
                new Vector2(0.0f, -20.0f),
                new Color4(1.0f, 1.0f, 1.0f, 0.0f),
                string.Format("map : {0} {1}", _i, _j));

            // now refine by selecting the nearest one of some neighbor tiles 
            Vector2 f2Cursor = new Vector2(_fX, _fY);
            float fMinDist = float.MaxValue;
            int offsetI = 0;
            int offsetJ = 0;
            if (false != this.GetSquareDistance(_i, _j, f2Cursor, ref fMinDist))
            {
                offsetI = 0;
                offsetJ = 0;
            }
            if ((_i < (this.MapWidth - 1)) && (false != this.GetSquareDistance(_i + 1, _j, f2Cursor, ref fMinDist)))
            {
                offsetI = 1;
                offsetJ = 0;
            }
            if ((_j < (this.MapHeight - 1)) && (false != this.GetSquareDistance(_i, _j + 1, f2Cursor, ref fMinDist)))
            {
                offsetI = 0;
                offsetJ = 1;
            }
            if (0 == (_j % 2))
            {
                if ((0 != _i) && (_j < (this.MapHeight - 1)) && (false != this.GetSquareDistance(_i - 1, _j + 1, f2Cursor, ref fMinDist)))
                {
                    offsetI = -1;
                    offsetJ = 1;
                }
            }
            else
            {
                if ((_i < (this.MapWidth - 1))
                    && (_j < (this.MapHeight - 1))
                    && (false != this.GetSquareDistance(_i + 1, _j + 1, f2Cursor, ref fMinDist)))
                {
                    offsetI = 1;
                    offsetJ = 1;
                }
            }
            _i = (uint)((int)_i + offsetI);
            _j = (uint)((int)_j + offsetJ);

            _mapCoords = new MapCoords(_i, _j);
        }

        protected bool GetSquareDistance(uint _i, uint _j, Vector2 _f2Cursor, ref float _fMinDist)
        {
            float fResult = Vector2.DistanceSquared(_f2Cursor, this.ComputePositionToWorld(new MapCoords(_i, _j), false));
            if (fResult < _fMinDist)
            {
                _fMinDist = fResult;
                return true;
            }
            return false;
        }

        protected void InitializeTiles(XmlElement _xmlTiles)
        {
            MapTileManager mapTileManager = this.m_coManager.CoreContext.GetData<MapTileManager>("MapTileManager");

            // mask
            this.m_strTileMask = _xmlTiles.Attributes["tilemask"].Value;
            this.m_maskTexture = this.m_coManager.CoreContext.GetData<TextureManager>("TextureManager").GetTexture(this.m_strTileMask);
            ImageInformation imgInfo = this.m_maskTexture.Info;
            if (imgInfo.Width != imgInfo.Height)
            {
                throw new Exception(string.Format("tile mask {0} is invalid : width and height must be equal."));
            }
            this.m_fMaskRadius = (float)imgInfo.Width / 2.0f;
            // now compute the offset vector used for index (i, j) to position (x, y) conversion.
            {
                // Altitude/height for an 1 unit equilateral triangle
                float fEquilateralTriangleAltitude = (float)Math.Sqrt(3) / 2.0f;
                float fAngle = (float)(-60.0 / 180.0 * Math.PI);
                float offsetX = (float)Math.Cos(fAngle);
                float offsetY = (float)Math.Sin(fAngle);
                this.m_f2MaskOffset.X = fEquilateralTriangleAltitude * offsetX * this.MaskRadius;
                this.m_f2MaskOffset.Y = fEquilateralTriangleAltitude * offsetY * this.MaskRadius;
            }

            // tile definitions
            XmlNodeList lstTiles = _xmlTiles.SelectNodes("tile");
            foreach (XmlElement xmlTile in lstTiles)
            {
                string tileID = xmlTile.Attributes["id"].Value;
                if (false != this.m_htblTiles.ContainsKey(tileID))
                {
                    throw new Exception(string.Format("{1} : tile definition {0} has already been declared.", tileID, this.m_strID));
                }
                string tileLocation = xmlTile.Attributes["location"].Value;
                MapTileDefinition mapTileDefinition = mapTileManager.Get(tileLocation);
                if (null == mapTileDefinition)
                {
                    mapTileDefinition = mapTileManager.LoadTile(tileLocation, this);
                    if (null == mapTileDefinition)
                    {
                        throw new Exception(string.Format("{1} : could not find or load {0} tile definition at {2}.", tileID, this.m_strID, tileLocation));
                    }
                }
                this.m_htblTiles.Add(tileID, mapTileDefinition);
            }

            // offset position to center the whole map
            this.m_f2CenterOffset = this.ComputePositionToWorld(new MapCoords(this.MapWidth - 1, this.MapHeight - 1));
            this.m_f2CenterOffset /= -2.0f;
        }

        protected void InitializeLayout(XmlElement _xmlLayout)
        {
            this.m_strDefaultTileID = _xmlLayout.Attributes["defauttileid"].Value;
            XmlNodeList lstTiles = _xmlLayout.SelectNodes("tile");
            foreach (XmlElement xmlTile in lstTiles)
            {
                string tileID = xmlTile.Attributes["id"].Value;
                if (false == this.m_htblTiles.ContainsKey(tileID))
                {
                    throw new Exception(string.Format("{1} : unknown layout tile {0}.", tileID, this.m_strID));
                }
                MapTileDefinition definition = this.m_htblTiles[tileID] as MapTileDefinition;
                if (false != this.m_htblLayout.ContainsKey(definition))
                {
                    throw new Exception(string.Format("{1} : layout for tile {0} has already been declared.", tileID, this.m_strID));
                }
                // layout data is stored in inner text part
                // format : x1 y1 x2 y2 ... xi yi
                char[] separators = { ' ', '\n', '\t', '\r' };
                string[] xyCoords = xmlTile.InnerText.Split(separators, StringSplitOptions.RemoveEmptyEntries);
                // array size must be even (since format is  : x y)
                if (0 != (xyCoords.Length % 2))
                {
                    throw new Exception(string.Format("{1} : layout for tile {0} is missing a coordinate info.", tileID, this.m_strID));
                }
                // store data in dedicated class
                MapCoords[] mapCoords = new MapCoords[xyCoords.Length / 2];
                for (int i = 0; (xyCoords.Length / 2) > i; ++i)
                {
                    mapCoords[i] = new MapCoords(uint.Parse(xyCoords[i * 2 + 0]), uint.Parse(xyCoords[i * 2 + 1]));
                }
                this.m_htblLayout.Add(definition, mapCoords);
            }
        }

        override public void Release()
        {
            base.Release();
            foreach (DictionaryEntry de in this.m_htblTiles)
            {
                (de.Value as MapTileDefinition).Release();
            }
            this.m_htblTiles.Clear();
            this.m_htblLayout.Clear();
        }
    }

    public class CoMap : Level.LevelComponentBase<CoMapProto>, IMapCommunicator
    {
        protected MapTile[,] m_tiles;

        public CoMap()
        {

        }

        override public bool Initialize(Entity _entity, IComponentPrototype _coProto)
        {
            if (false != base.Initialize(_entity, _coProto))
            {
                // create tiles
                this.m_tiles = new MapTile[this.m_coProto.MapWidth, this.m_coProto.MapHeight];

                // first initialize tiles with explicitly declared layouts.
                foreach (DictionaryEntry de in this.m_coProto.Layout)
                {
                    MapTileDefinition definition = de.Key as MapTileDefinition;
                    MapCoords[] arrMapCoords = de.Value as MapCoords[];
                    foreach (MapCoords mapCoords in arrMapCoords)
                    {
                        MapTile mapTile = new MapTile(definition, this.m_coProto);
                        mapTile.Position = this.m_coProto.ComputePositionToWorld(mapCoords);
                        mapTile.MapCommunicator = this;
                        mapTile.Coords = new MapCoords(mapCoords);
                        this.m_tiles[mapCoords.X, mapCoords.Y] = mapTile;
                    }
                }

                // then fill remaining tiles with default tile definition.
                MapCoords mc = new MapCoords();
                for (uint j = 0; this.m_coProto.MapHeight > j; ++j)
                {
                    mc.Y = j;
                    for (uint i = 0; this.m_coProto.MapWidth > i; ++i)
                    {
                        if (null == this.m_tiles[i, j])
                        {
                            mc.X = i;
                            MapTile mapTile = new MapTile(this.m_coProto.GetDefaultTile(), this.m_coProto);
                            mapTile.Position = this.m_coProto.ComputePositionToWorld(mc);
                            mapTile.MapCommunicator = this;
                            mapTile.Coords = new MapCoords(mc);
                            this.m_tiles[i, j] = mapTile;
                        }
                    }
                }

                // let know each tile which renderers it can work with
                Module module = this.m_coProto.Manager.CoreContext.GetData<Module>("micivCore.Module");
                foreach (MapTile tile in this.m_tiles)
                {
                    tile.RestrictedRendererContexts = module.RestrictedRendererContextes;
                }
                return true;
            }
            return false;
        }

        override public void PostInitialize(XmlElement _xmlComponent)
        {
            base.PostInitialize(_xmlComponent);
            this.Level.MapCommunicator = this;
        }

        override public void Update(float _deltaTime)
        {
            base.Update(_deltaTime);
            foreach (MapTile tile in this.m_tiles)
            {
                tile.Update(_deltaTime);
            }
        }

        override public void Release()
        {
            base.Release();
            foreach (MapTile tile in this.m_tiles)
            {
                tile.Release();
            }
        }

        public Vector2 ComputeMapCoordToWorld(MapCoords _mapCoords)
        {
            return this.m_coProto.ComputePositionToWorld(_mapCoords);
        }

        public void ComputeWorldToMapCoord(float _fX, float _fY, out MapCoords _mapCoords)
        {
            this.m_coProto.ComputeWorldToPosition(_fX, _fY, out _mapCoords);
        }

        public void SetVisibleMapTile(MapCoords _mapCoords, string _strRenderable, bool _bState)
        {
            if ((this.m_coProto.MapWidth > _mapCoords.X) && (this.m_coProto.MapHeight > _mapCoords.Y))
            {
                MapTile tile = this.m_tiles[_mapCoords.X, _mapCoords.Y];
                tile.SetVisible(_strRenderable, _bState);
            }
        }

        public void AttachUnitToMapTile(MapCoords _mapCoords, IUnit _unit)
        {
            if ((this.m_coProto.MapWidth > _mapCoords.X) && (this.m_coProto.MapHeight > _mapCoords.Y))
            {
                MapTile tile = this.m_tiles[_mapCoords.X, _mapCoords.Y];
                tile.AttachUnit(_unit);
            }
        }

        public void DetachUnitToMapTile(MapCoords _mapCoords, IUnit _unit)
        {
            if ((this.m_coProto.MapWidth > _mapCoords.X) && (this.m_coProto.MapHeight > _mapCoords.Y))
            {
                MapTile tile = this.m_tiles[_mapCoords.X, _mapCoords.Y];
                tile.DetachUnit(_unit);
            }
        }

        public MapTile GetMapTile(MapCoords _mapCoords)
        {
            if ((this.m_coProto.MapWidth > _mapCoords.X) && (this.m_coProto.MapHeight > _mapCoords.Y))
            {
                return this.m_tiles[_mapCoords.X, _mapCoords.Y];
            }
            return null;
        }

        public void GetNeighbors(MapCoords _mapCoords, ref List<MapTile> _lstNeightbors)
        {
            if ((this.m_coProto.MapWidth > _mapCoords.X) && (this.m_coProto.MapHeight > _mapCoords.Y))
            {
                if (0 == (_mapCoords.Y % 2))
                {
                    if (0 != _mapCoords.X)
                    {
                        _lstNeightbors.Add(this.m_tiles[_mapCoords.X - 1, _mapCoords.Y]);
                    }
                    if ((this.m_coProto.MapWidth - 1) != _mapCoords.X)
                    {
                        _lstNeightbors.Add(this.m_tiles[_mapCoords.X + 1, _mapCoords.Y]);
                    }
                    if (0 != _mapCoords.Y)
                    {
                        _lstNeightbors.Add(this.m_tiles[_mapCoords.X, _mapCoords.Y - 1]);
                        if (0 != _mapCoords.X)
                        {
                            _lstNeightbors.Add(this.m_tiles[_mapCoords.X - 1, _mapCoords.Y - 1]);
                        }
                    }
                    if ((this.m_coProto.MapHeight - 1) != _mapCoords.Y)
                    {
                        _lstNeightbors.Add(this.m_tiles[_mapCoords.X, _mapCoords.Y + 1]);
                        if (0 != _mapCoords.X)
                        {
                            _lstNeightbors.Add(this.m_tiles[_mapCoords.X - 1, _mapCoords.Y + 1]);
                        }
                    }
                }
                else
                {
                    if (0 != _mapCoords.X)
                    {
                        _lstNeightbors.Add(this.m_tiles[_mapCoords.X - 1, _mapCoords.Y]);
                    }
                    if ((this.m_coProto.MapWidth - 1) != _mapCoords.X)
                    {
                        _lstNeightbors.Add(this.m_tiles[_mapCoords.X + 1, _mapCoords.Y]);
                    }
                    if (0 != _mapCoords.Y)
                    {
                        _lstNeightbors.Add(this.m_tiles[_mapCoords.X, _mapCoords.Y - 1]);
                        if ((this.m_coProto.MapWidth - 1) != _mapCoords.X)
                        {
                            _lstNeightbors.Add(this.m_tiles[_mapCoords.X + 1, _mapCoords.Y - 1]);
                        }
                    }
                    if ((this.m_coProto.MapHeight - 1) != _mapCoords.Y)
                    {
                        _lstNeightbors.Add(this.m_tiles[_mapCoords.X, _mapCoords.Y + 1]);
                        if ((this.m_coProto.MapWidth - 1) != _mapCoords.X)
                        {
                            _lstNeightbors.Add(this.m_tiles[_mapCoords.X + 1, _mapCoords.Y + 1]);
                        }
                    }
                }
            }
        }
    }
}
